/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.flink.connector.jdbc.internal.executor;

import org.apache.flink.annotation.Internal;
import org.apache.flink.connector.jdbc.JdbcStatementBuilder;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import static org.apache.flink.util.Preconditions.checkNotNull;

/**
 * {@link JdbcBatchStatementExecutor} that provides upsert semantics by updating row if it exists
 * and inserting otherwise. Used in Table API.
 * {@link JdbcBatchStatementExecutor}，通过更新行（如果存在）并插入否则提供更新插入语义。用于表 API。
 *
 * @deprecated This has been replaced with {@link TableInsertOrUpdateStatementExecutor}
 */
@Internal
public final class InsertOrUpdateJdbcExecutor<R, K, V> implements JdbcBatchStatementExecutor<R> {

    private static final Logger LOG = LoggerFactory.getLogger(InsertOrUpdateJdbcExecutor.class);

    private final String existSQL;
    private final String insertSQL;
    private final String updateSQL;

    private final JdbcStatementBuilder<K> existSetter;
    private final JdbcStatementBuilder<V> insertSetter;
    private final JdbcStatementBuilder<V> updateSetter;

    private final Function<R, K> keyExtractor;
    private final Function<R, V> valueMapper;

    private final Map<K, V> batch;

    private transient PreparedStatement existStatement;
    private transient PreparedStatement insertStatement;
    private transient PreparedStatement updateStatement;

    /**
     * 这是一个Java构造函数，它接受多个参数，并用于创建InsertOrUpdateJdbcExecutor对象。
     * 通过使用InsertOrUpdateJdbcExecutor构造函数，可以创建一个InsertOrUpdateJdbcExecutor对象，
     * 并使用它执行插入或更新操作。
     * 
     * @param existSQL
     * @param insertSQL
     * @param updateSQL
     * @param existSetter
     * @param insertSetter
     * @param updateSetter
     * @param keyExtractor
     * @param valueExtractor
     */
    public InsertOrUpdateJdbcExecutor(
            @Nonnull String existSQL,
            @Nonnull String insertSQL,
            @Nonnull String updateSQL,
            @Nonnull JdbcStatementBuilder<K> existSetter,
            @Nonnull JdbcStatementBuilder<V> insertSetter,
            @Nonnull JdbcStatementBuilder<V> updateSetter,
            @Nonnull Function<R, K> keyExtractor,
            @Nonnull Function<R, V> valueExtractor) {
        // 使用 checkNotNull()方法检查所有参数是否为 null，并将它们分别赋值给类的成员变量。
        // existSQL、insertSQL和 updateSQL分别表示存在检查、插入和更新操作的 SQL语句；
        this.existSQL = checkNotNull(existSQL);
        this.insertSQL = checkNotNull(insertSQL);
        this.updateSQL = checkNotNull(updateSQL);
        // existSetter、insertSetter和 updateSetter是用于设置 SQL语句和查询参数的函数式接口；
        this.existSetter = checkNotNull(existSetter);
        this.insertSetter = checkNotNull(insertSetter);
        this.updateSetter = checkNotNull(updateSetter);
        // keyExtractor和 valueExtractor是用于从记录中提取主键和值的函数式接口；
        this.keyExtractor = checkNotNull(keyExtractor);
        this.valueMapper = checkNotNull(valueExtractor);
        // batch是一个 HashMap对象，用于存储批处理列表中的键值对。
        this.batch = new HashMap<>();
    }

    /**
     * 这段代码是一个方法，用于准备数据库连接中的预编译语句。
     * 
     * @param connection
     * @throws SQLException
     *        如果在准备过程中发生了SQLException异常，则需要在调用该方法时进行异常处理。
     */
    @Override
    public void prepareStatements(Connection connection) throws SQLException {
        // 通过使用 connection.prepareStatement()方法，
        // 分别为 existSQL、insertSQL和 updateSQL创建了相应的预编译语句对象 
        // existStatement、insertStatement和updateStatement。
        existStatement = connection.prepareStatement(existSQL);
        insertStatement = connection.prepareStatement(insertSQL);
        updateStatement = connection.prepareStatement(updateSQL);
        
        // 这些预编译语句对象可以被用于执行具体的SQL操作，如查询、插入或更新等。
        // 通过准备好这些预编译语句对象，可以提高数据库操作的性能和安全性。
    }

    /**
     * 这是一个Java方法，它实现了 addToBatch()接口，用于将记录添加到批处理列表中。
     * 
     * @param record
     */
    @Override
    public void addToBatch(R record) {
        
        // 该方法接受一个泛型类型的记录作为参数，
        // 并使用 keyExtractor和 valueMapper 函数式接口从记录中提取主键和值，
        // 并将它们作为键值对添加到批处理列表中。
        batch.put(keyExtractor.apply(record), valueMapper.apply(record));
        // 在该方法中，keyExtractor和 valueMapper函数式接口是通过其他方法或类传递进来的，
        // 它们用于从记录中提取主键和值。
        // 通过使用 addToBatch()方法，可以将多个记录按顺序添加到批处理列表中，
        // 然后一次性执行它们。
    }

    /**
     * 这是一个 Java方法，它实现了 executeBatch()接口，用于执行批处理操作。
     * 
     * @throws SQLException
     */
    @Override
    public void executeBatch() throws SQLException {
        //如果批处理列表不为空，
        if (!batch.isEmpty()) {
            
            // 则遍历批处理列表中的每个元素，
            for (Map.Entry<K, V> entry : batch.entrySet()) {
                
                // 并将其传递给 processOneRowInBatch()方法进行处理。
                processOneRowInBatch(entry.getKey(), entry.getValue());
            }
            
            // 在处理完所有的批处理之后，
            // 使用updateStatement.executeBatch()执行更新操作的批处理，
            updateStatement.executeBatch();
            
            // 使用insertStatement.executeBatch()方法执行插入操作的批处理，
            insertStatement.executeBatch();
            
            // 然后清空批处理列表。
            batch.clear();
        }
        // 请注意，如果批处理列表为空，则不执行任何操作。
        // 如果在执行过程中发生 SQLException异常，则需要在调用该方法时进行异常处理。
    }

    /**
     * 通过这种方式，可以根据数据库中是否存在记录来决定执行更新操作还是插入操作，
     * 并将相应的语句添加到批处理中。
     * 
     * @param pk：表示受一个主键 pk；
     * @param row：表示一行数据 row；
     * 
     * @throws SQLException
     */
    private void processOneRowInBatch(K pk, V row) throws SQLException {
        //首先通过调用exist(pk)方法检查数据库中是否存在具有给定主键的记录。
        if (exist(pk)) {
            // 这行代码是一个函数式编程中的 Lambda表达式，
            // 它将updateStatement和row作为参数传递给updateSetter函数式接口的accept方法。
            // 在这里，updateSetter 是一个函数式接口，
            // 它接受一个PreparedStatement对象和一个表示行数据的对象作为参数，并没有返回值。
            // accept方法会调用 PreparedStatement对象的 set方法，
            // 将行数据设置到相应的查询参数中，以便执行更新操作。

            // 如果存在，则执行更新操作，
            // 调用 updateSetter.accept(updateStatement, row)方法将行数据设置到更新语句中，
            updateSetter.accept(updateStatement, row);
            
            // 这行代码用于将当前的更新语句添加到批处理中。
            // 批处理是一种将多个 SQL语句一起发送到数据库执行的机制，可以提高数据库操作的性能。
            // 通过使用 addBatch()方法，可以将多个更新语句按顺序添加到批处理中，
            // 然后一次性执行它们。
            // 在执行批处理之前，还需要调用 executeBatch()方法来实际执行这些更新语句。
            // 使用批处理可以减少与数据库的通信次数，从而提高整体的执行效率。
            
            // 并使用 updateStatement.addBatch() 将更新语句添加到批处理中。
            updateStatement.addBatch();
        } else {
            // 这行代码是一个函数式编程中的 Lambda 表达式，
            // 它将 insertStatement和 row 作为参数传递给 insertSetter函数式接口的 accept方法。
            // 在这里，insertSetter 是一个函数式接口，
            // 它接受一个 PreparedStatement 对象和一个表示行数据的对象作为参数，并没有返回值。
            // accept方法会调用 PreparedStatement 对象的 set方法，
            // 将行数据设置到相应的查询参数中，以便执行插入操作。
            
            // 如果不存在具有给定主键的记录，则执行插入操作，
            // 调用 insertSetter.accept(insertStatement, row)方法将行数据设置到插入语句中，
            insertSetter.accept(insertStatement, row);
            
            // 并使用 insertStatement.addBatch()将插入语句添加到批处理中。
            insertStatement.addBatch();
        }
    }

    /**
     * 检查具有给定主键值的行是否存在于数据库中。如果存在，则返回 true，否则返回 false。
     * 
     * @param pk
     * @return
     * @throws SQLException
     * 
     * 请注意，该方法可能会抛出SQLException异常，因为执行SQL查询可能会失败。
     * 在调用该方法时，需要处理SQLException异常。
     */
    private boolean exist(K pk) throws SQLException {
        // 这行代码是一个函数式编程中的 Lambda 表达式，
        // 它将 existStatement和 pk 作为参数传递给 existSetter函数式接口的 accept 方法。
        // 在这里，existSetter是一个函数式接口，
        // 它接受一个 PreparedStatement对象和一个泛型类型的参数K，并没有返回值。
        // accept 方法会调用 PreparedStatement对象的 set 方法设置查询参数，
        // 以便在查询中使用给定的主键值。
        existSetter.accept(existStatement, pk);
        
        // 这段代码使用 try-with-resources结构创建一个 ResultSet对象，
        // 并使用 existStatement执行查询。
        // ResultSet对象包含查询结果集，并且可以使用 next()方法检索结果集中的下一行。
        try (ResultSet resultSet = existStatement.executeQuery()) {
            
            // 在这里，使用 next()方法检查结果集中是否存在一行数据，
            // 如果存在则返回 true，否则返回 false。
            // 由于 ResultSet实现了 AutoCloseable 接口，
            // 因此在 try-with-resources 结构中使用 ResultSet 对象时，
            // 无需手动关闭 ResultSet对象，它会在 try 块结束时自动关闭。
            return resultSet.next();
        }
    }

    @Override
    public void closeStatements() throws SQLException {
        // 这段代码使用了增强的 for循环，遍历了
        // 一个包含 existStatement、insertStatement 和 updateStatement
        // 的 PreparedStatement对象列表。
        
        // 在循环的每次迭代中，将当前的 PreparedStatement对象赋值给变量 s，并执行相应的操作。
        // 你可以在注释的位置添加具体的操作代码。
        // 通过这种方式，可以方便地对多个 PreparedStatement对象进行相同或类似的操作。
        for (PreparedStatement s :
                Arrays.asList(existStatement, insertStatement, updateStatement)) {
            // 这段代码用于检查 PreparedStatement对象 s 是否为 null，
            // 如果不为 null，
            if (s != null) {
                //则调用其 close()方法关闭该对象。
                s.close();
                // 通过这个判断和关闭操作，
                // 可以确保在使用完 PreparedStatement 对象后及时释放相关资源，
                // 以避免资源泄漏和提高代码的健壮性。
            }
        }
    }
}
